/*

 * Copyright (C) 2021 Huawei Device Co., Ltd.

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package me.panpf.sketch.sample.vt.ui.listcontainer;

import ohos.agp.components.*;
import ohos.agp.database.DataSetSubscriber;
import ohos.app.Context;
import ohos.multimodalinput.event.TouchEvent;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import static ohos.agp.utils.LayoutAlignment.TOP;

/**
 * me.panpf.sketch.sample.vt.ui.listcontainer
 *
 * @author hw
 * @since 2021/6/23
 */
public class RefreshListContainer extends ListContainer implements Component.LayoutRefreshedListener,
        Component.TouchEventListener {
    // 如果用户的adapter与它们重复将会强制抛出异常。为了简化,我们检测到重复时对用户的提示是ItemViewType必须小于10000
    private static final int TYPE_REFRESH_HEADER = 10000; // 设置一个很大的数字,尽可能避免和用户的adapter冲突
    private static final int TYPE_FOOTER = 10001;
    private static final int HEADER_INIT_INDEX = 10002;
    private static List<Integer> sHeaderTypes = new ArrayList<>(); // 每个header必须有不同的type,不然滚动的时候顺序会变化
    private static ArrayList<Component> mHeaderViews = new ArrayList<>();

    // limit number to call load more
    // 控制多出多少条的时候调用 onLoadMore
    private static int height;
    private float mLastY = -1;
    private float dragRate = 3;

    private BaseItemProvider mWrapAdapter;

    /**
     * ArrowRefreshHeader
     *
     * @return ArrowRefreshHeader
     */
    public static ArrowRefreshHeader getmRefreshHeader() {
        return mRefreshHeader;
    }

    private static ArrowRefreshHeader mRefreshHeader;
    private static LoadingMoreFooter footView;
    private LoadingListener mLoadingListener;
    private ScrollHelper mScrollhelper;
    private DataSetSubscriber mDataObserver = new DataObserver();

    // adapter没有数据的时候显示,类似于listView的emptyView
    private Component mEmptyView;
    private Component mFootView;
    private CustomFooterViewCallBack footerViewCallBack;

    // 是否下拉刷新
    private boolean pullRefreshEnabled = true;
    private boolean refreshEnabled = false;

    // 上拉刷新加载更多数据
    private static boolean loadingMoreEnabled = true;
    private boolean isNoMore = false;
    private Status mStatus = Status.NORMAL;

    /**
     * Status
     */
    public enum Status {
        REFRESHING,
        REFRESH_COMPLETE,
        LOAD_COMPLETE,
        LOADING_MORE,
        NORMAL
    }

    public RefreshListContainer(Context context) {
        super(context);
        init(context);
    }

    public RefreshListContainer(Context context, AttrSet attrSet) {
        super(context, attrSet);
        init(context);
    }

    public RefreshListContainer(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        init(context);
    }

    private void init(Context context) {
        mHeaderViews.clear();
        mScrollhelper = new ScrollHelper();
        setTouchEventListener(this);
        setLayoutRefreshedListener(this);
        if (pullRefreshEnabled) {
            mRefreshHeader = new ArrowRefreshHeader(context);
        }
        footView = new LoadingMoreFooter(context);
        mFootView = footView;
        mFootView.setVisibility(HIDE);
    }

    /**
     * call it when you finish the ability,
     * when you call this,better don't call some kind of functions like
     * RefreshHeader,because the reference of mHeaderViews is NULL.
     */
    public void destroy() {
        if (mHeaderViews != null) {
            mHeaderViews.clear();
            mHeaderViews = null;
        }
        if (mFootView instanceof LoadingMoreFooter) {
            ((LoadingMoreFooter) mFootView).destroy();
            mFootView = null;
        }
        if (mRefreshHeader != null) {
            mRefreshHeader.destroy();
            mRefreshHeader = null;
        }
    }

    /**
     * getDefaultRefreshHeaderView
     *
     * @return ArrowRefreshHeader
     */
    public ArrowRefreshHeader getDefaultRefreshHeaderView() {
        if (mRefreshHeader == null) {
            return null;
        }
        return mRefreshHeader;
    }

    /**
     * getDefaultFootView
     *
     * @return LoadingMoreFooter
     */
    public LoadingMoreFooter getDefaultFootView() {
        if (mFootView == null) {
            return null;
        }
        if (mFootView instanceof LoadingMoreFooter) {
            return ((LoadingMoreFooter) mFootView);
        }
        return null;
    }

    /**
     * setLimitNumberToCallLoadMore
     *
     * @param limitNumberToCallLoadMore int
     */
    public void setLimitNumberToCallLoadMore(int limitNumberToCallLoadMore) {
    }

    /**
     * getFootView
     *
     * @return getFootView
     */
    public Component getFootView() {
        return mFootView;
    }

    /**
     * setFootViewText
     *
     * @param loading loading
     * @param noMore  noMore
     */
    public void setFootViewText(String loading, String noMore) {
        if (mFootView instanceof LoadingMoreFooter) {
            ((LoadingMoreFooter) mFootView).setLoadingHint(loading);
            ((LoadingMoreFooter) mFootView).setNoMoreHint(noMore);
        }
    }

    /**
     * setFootView
     *
     * @param view               view
     * @param footerViewCallBack footerViewCallBack
     */
    @SuppressWarnings("all")
    public void setFootView(final Component view, CustomFooterViewCallBack footerViewCallBack) {
        if (view == null || footerViewCallBack == null) {
            return;
        }
        mFootView = view;
        this.footerViewCallBack = footerViewCallBack;
    }

    /**
     * Fix issues (多个地方使用该控件的时候，所有刷新时间都相同 #359)
     *
     * @param keyName keyName
     */
    public void setRefreshTimeSpKeyName(String keyName) {
        if (mRefreshHeader != null) {
            mRefreshHeader.setXrRefreshTimeKey(keyName);
        }
    }

    /**
     * loadMoreComplete
     */
    public void loadMoreComplete() {
        mStatus = Status.LOAD_COMPLETE;
        if (mFootView instanceof LoadingMoreFooter) {
            ((LoadingMoreFooter) mFootView).setState(LoadingMoreFooter.STATE_COMPLETE);
        } else {
            if (footerViewCallBack != null) {
                footerViewCallBack.onLoadMoreComplete(mFootView);
            }
        }
    }

    /**
     * setNoMore
     *
     * @param noMore noMore
     */
    public void setNoMore(boolean noMore) {
        isNoMore = noMore;
        if (mFootView instanceof LoadingMoreFooter) {
            ((LoadingMoreFooter) mFootView).setState(isNoMore ? LoadingMoreFooter.STATE_NOMORE : LoadingMoreFooter.STATE_COMPLETE);
        } else {
            if (footerViewCallBack != null) {
                footerViewCallBack.onSetNoMore(mFootView, noMore);
            }
        }
    }

    /**
     * refresh
     */
    public void refresh() {
        if (pullRefreshEnabled && mLoadingListener != null) {
            mRefreshHeader.setState(ArrowRefreshHeader.STATE_REFRESHING);
            mLoadingListener.onRefresh();
        }
    }

    /**
     * reset
     */
    public void reset() {
        setNoMore(false);
        loadMoreComplete();
        refreshComplete();
    }

    /**
     * refreshComplete
     */
    public void refreshComplete() {
        if (mRefreshHeader != null) {
            mStatus = Status.REFRESH_COMPLETE;
            mRefreshHeader.refreshComplete();
        }
        if (mWrapAdapter != null) {
            mWrapAdapter.notifyDataChanged();
        }
        setNoMore(false);
    }

    /**
     * setRefreshHeader
     *
     * @param refreshHeader refreshHeader
     */
    public void setRefreshHeader(ArrowRefreshHeader refreshHeader) {
        setRefreshHeaderValue(refreshHeader);
    }

    /**
     * setRefreshHeaderValue
     *
     * @param refreshHeader refreshHeader
     */
    public static void setRefreshHeaderValue(ArrowRefreshHeader refreshHeader) {
        RefreshListContainer.mRefreshHeader = refreshHeader;
    }

    /**
     * setPullRefreshEnabled
     *
     * @param enabled enabled
     */
    public void setPullRefreshEnabled(boolean enabled) {
        pullRefreshEnabled = enabled;
    }

    /**
     * setLoadingMoreEnabled
     *
     * @param enabled enabled
     */
    public void setLoadingMoreEnabled(boolean enabled) {
        setLoadingMoreEnabledValue(enabled);
        if (!enabled) {
            if (mFootView instanceof LoadingMoreFooter) {
                ((LoadingMoreFooter) mFootView).setState(LoadingMoreFooter.STATE_COMPLETE);
            }
        }
    }

    /**
     * setLoadingMoreEnabledValue
     *
     * @param enabled enabled
     */
    public static void setLoadingMoreEnabledValue(boolean enabled) {
        RefreshListContainer.loadingMoreEnabled = enabled;
    }

    /**
     * setRefreshProgressStyle
     *
     * @param style style
     */
    public void setRefreshProgressStyle(int style) {
        if (mRefreshHeader != null) {
            mRefreshHeader.setProgressStyle(style);
        }
    }

    /**
     * setRefreshProgressIndicatorColor
     *
     * @param color color
     */
    public void setRefreshProgressIndicatorColor(int color) {
        if (mRefreshHeader != null) {
            mRefreshHeader.setIndicatorColor(color);
        }
    }

    /**
     * setLoadingMoreProgressStyle
     *
     * @param style style
     */
    public void setLoadingMoreProgressStyle(int style) {
        if (mFootView instanceof LoadingMoreFooter) {
            ((LoadingMoreFooter) mFootView).setProgressStyle(style);
        }
    }

    /**
     * 设置下拉时图图标样式
     *
     * @param resId resId
     */
    public void setArrowImageView(int resId) {
        if (mRefreshHeader != null) {
            mRefreshHeader.setArrowImageView(resId);
        }
    }

    /**
     * 设置下拉时候的偏移计量因子。y = deltaY/dragRate
     *
     * @param rate rate
     */
    public void setDragRate(float rate) {
        if (rate <= 0.5) {
            return;
        }
        dragRate = rate;
    }

    // if you can't sure that you are 100% going to
    // have no data load back from server anymore,do not use this
    @Deprecated
    public void setEmptyView(Component emptyView) {
        this.mEmptyView = emptyView;
        mDataObserver.onChanged();
    }

    @Deprecated
    public Component getEmptyView() {
        return mEmptyView;
    }

    // 上拉加载更多数据
    private void initComputeScroll() {
        setScrolledListener(new ScrolledListener() {
            @Override
            public void onContentScrolled(Component component, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                if (mScrollhelper.isFinished()) {
                    int lastIndex = getLastVisibleItemPosition();
                    int adjLen = (loadingMoreEnabled ? 2 : 1);
                    if (mLoadingListener != null && mStatus != Status.REFRESHING
                            && mStatus != Status.LOADING_MORE && loadingMoreEnabled
                            && lastIndex == mWrapAdapter.getCount() - adjLen) {
                        if (footView.getVisibility() == HIDE) {
                            mStatus = Status.LOADING_MORE;
                            if (mFootView instanceof LoadingMoreFooter) {
                                ((LoadingMoreFooter) mFootView).setState(LoadingMoreFooter.STATE_LOADING);
                            } else {
                                if (footerViewCallBack != null) {
                                    footerViewCallBack.onLoadingMore(mFootView);
                                }
                            }
                        }

                        if (footView.getVisibility() == VISIBLE) {
                            mLoadingListener.onLoadMore();
                        }
                    }
                }
            }
        });
    }

    /**
     * setLoadingListener
     *
     * @param listener listener
     */
    public void setLoadingListener(LoadingListener listener) {
        mLoadingListener = listener;
    }

    /**
     * LoadingListener
     */
    public interface LoadingListener {
        void onRefresh();

        void onLoadMore();
    }

    @Override
    public void setItemProvider(BaseItemProvider itemProvider) {
        mWrapAdapter = itemProvider;
        super.setItemProvider(itemProvider);
    }

    @Override
    public BaseItemProvider getItemProvider() {
        return super.getItemProvider();
    }

    @Override
    public void onRefreshed(Component component) {
        initComputeScroll();
        invalidate();
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent ev) {
        int firstIndex = getItemPosByVisibleIndex(0);
        switch (ev.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                mLastY = ev.getPointerScreenPosition(0).getY();
                break;
            case TouchEvent.POINT_MOVE:
                if (mRefreshHeader != null && mRefreshHeader.getState() == ArrowRefreshHeader.STATE_REFRESHING && mRefreshHeader.getVisibleHeight() != 0) {
                    return false;
                }
                float deltaY = (float) (ev.getPointerScreenPosition(0).getY() - (double) mLastY);
                if (pullRefreshEnabled && firstIndex <= 1) {
                    refreshEnabled = true;
                    if (mRefreshHeader == null) {
                        break;
                    }
                    mRefreshHeader.onMove(deltaY / dragRate);
                    mWrapAdapter.notifyDataSetItemRangeChanged(0, 1);
                    if (mRefreshHeader.getVisibleHeight() > 0 && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {
                        return true;
                    }
                } else {
                    mLastY = ev.getPointerScreenPosition(0).getY();
                }
                break;
            case TouchEvent.CANCEL:
            case TouchEvent.PRIMARY_POINT_UP:
                mLastY = -1;
                if (pullRefreshEnabled && refreshEnabled) {
                    refreshEnabled = false;
                    if (mRefreshHeader != null && mRefreshHeader.releaseAction() && firstIndex == 0) {
                        if (mLoadingListener != null) {
                            mStatus = Status.REFRESHING;
                            mLoadingListener.onRefresh();
                            mWrapAdapter.notifyDataChanged();
                        }
                    } else {
                        mWrapAdapter.notifyDataChanged();
                    }
                }
                break;
        }
        return true;
    }

    /**
     * addHeaderView
     *
     * @param view addHeaderView
     */
    public void addHeaderView(Component view) {
        if (mHeaderViews == null || sHeaderTypes == null) {
            return;
        }
        sHeaderTypes.add(HEADER_INIT_INDEX + mHeaderViews.size());
        mHeaderViews.add(view);
        if (mWrapAdapter != null) {
            mWrapAdapter.notifyDataChanged();
        }
    }

    /**
     * removeHeaderView
     *
     * @param component component
     */
    public void removeHeaderView(Component component) {
        if (mHeaderViews == null || sHeaderTypes == null || component == null) {
            return;
        }
        for (Component view : mHeaderViews) {
            if (view == component) {
                mHeaderViews.remove(view);
                break;
            }
        }
        if (mWrapAdapter != null) {
            mWrapAdapter.notifyDataChanged();
        }
    }

    /**
     * removeAllHeaderView
     */
    public void removeAllHeaderView() {
        if (mHeaderViews == null || sHeaderTypes == null) {
            return;
        }
        mHeaderViews.clear();
        if (mWrapAdapter != null) {
            mWrapAdapter.notifyDataChanged();
        }
    }

    /**
     * =======================================================  ItemProvider start=======================================================
     */
    public static abstract class RecyclerAdapter<T> extends BaseItemProvider {
        private Context context;
        private List<T> data;
        private int mLayoutId;
        private int numColumns = 1;
        private OnItemClickListener onItemClickListener;

        public RecyclerAdapter(Context context, List<T> data, int mLayoutId) {
            this.context = context;
            this.data = data;
            this.mLayoutId = mLayoutId;
        }

        /**
         * getData
         *
         * @return getData
         */
        public List<T> getData() {
            return data;
        }

        @Override
        public int getCount() {
            int adjLen = (loadingMoreEnabled ? 2 : 1) + mHeaderViews.size();
            return (data.size() % numColumns == 0 ? data.size() / numColumns : data.size() / numColumns + 1) + adjLen;
        }

        @Override
        public T getItem(int position) {
            return data.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public int getItemComponentType(int position) {
            if (position == getCount() - 1) {
                return TYPE_FOOTER;
            }
            return super.getItemComponentType(position);
        }

        @Override
        public Component getComponent(int position, Component convertComponent, ComponentContainer parent) {
            if (position == 0) {
                return mRefreshHeader;
            } else if (position > 0 && position < mHeaderViews.size() + 1) {
                Component component = mHeaderViews.get(position - 1);
                return component;
            } else if (position == getCount() - 1) {
                return footView;
            }
            ViewHolder viewHolder;
            convertComponent = new DirectionalLayout(context);
            ((DirectionalLayout) convertComponent).setOrientation(Component.HORIZONTAL);
            ComponentContainer.LayoutConfig layoutConfig = convertComponent.getLayoutConfig();
            layoutConfig.width = DependentLayout.LayoutConfig.MATCH_PARENT;
            convertComponent.setLayoutConfig(layoutConfig);
            if ((position - 1) < data.size()) {
                DirectionalLayout dlItemParent = new DirectionalLayout(context);
                dlItemParent.setLayoutConfig(new DirectionalLayout.LayoutConfig(0,
                        DirectionalLayout.LayoutConfig.MATCH_CONTENT, TOP, 1));
                height = dlItemParent.getHeight();
                Component childConvertComponent = LayoutScatter.getInstance(context).parse(mLayoutId,
                        null, false);
                childConvertComponent.setClickedListener(new Component.ClickedListener() {
                    @Override
                    public void onClick(Component component) {
                        onItemClickListener.onItemClick(component, (position - 1) * numColumns);
                    }
                });
                dlItemParent.addComponent(childConvertComponent);
                ((ComponentContainer) convertComponent).addComponent(dlItemParent);
                viewHolder = new ViewHolder(childConvertComponent);
                bind(viewHolder, getItem((position - 1)),
                        (position - 1));
            } else {
                Component childConvertComponent = new Component(context);
                DirectionalLayout.LayoutConfig layoutConfig1 = new DirectionalLayout.LayoutConfig(0, height);
                layoutConfig1.weight = 1;
                childConvertComponent.setLayoutConfig(layoutConfig1);
                ((ComponentContainer) convertComponent).addComponent(childConvertComponent);
            }
            return convertComponent;
        }

        /**
         * setNumColumns
         *
         * @param numColumns numColumns
         */
        public void setNumColumns(int numColumns) {
            this.numColumns = numColumns;
        }

        /**
         * setOnItemClickListener
         *
         * @param onItemClickListener setOnItemClickListener
         */
        public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
            this.onItemClickListener = onItemClickListener;
        }

        /**
         * bind
         *
         * @param holder   holder
         * @param s1       s1
         * @param position position
         */
        public abstract void bind(ViewHolder holder, T s1, int position);

        /**
         * ViewHolder
         */
        public class ViewHolder {
            HashMap<Integer, Component> mViews = new HashMap<>();
            public Component itemView;

            public ViewHolder(Component component) {
                this.itemView = component;
            }

            public <E extends Component> E getView(int viewId) {
                E view = (E) mViews.get(viewId);
                if (view == null) {
                    view = (E) itemView.findComponentById(viewId);
                    mViews.put(viewId, view);
                }
                return view;
            }
        }

        /**
         * OnItemClickListener
         */
        public abstract static class OnItemClickListener {
            public abstract void onItemClick(Component component, int position);
        }

    }

    /**
     * =======================================================  ItemProvider end=======================================================
     */
    private class DataObserver extends DataSetSubscriber {
        @Override
        public void onChanged() {
            if (mWrapAdapter != null) {
                mWrapAdapter.notifyDataChanged();
            }
        }

        @Override
        public void onItemChanged(int position) {
            mWrapAdapter.notifyDataSetItemChanged(position);
        }

        @Override
        public void onItemInserted(int position) {
            mWrapAdapter.notifyDataSetItemInserted(position);
        }

        @Override
        public void onItemRemoved(int position) {
            mWrapAdapter.notifyDataSetItemRemoved(position);
        }

        @Override
        public void onItemRangeChanged(int startPos, int countItems) {
            mWrapAdapter.notifyDataSetItemRangeChanged(startPos, countItems);
        }

        @Override
        public void onItemRangeInserted(int startPos, int countItems) {
            mWrapAdapter.notifyDataSetItemRangeInserted(startPos, countItems);
        }

        @Override
        public void onItemRangeRemoved(int startPos, int countItems) {
            mWrapAdapter.notifyDataSetItemRangeRemoved(startPos, countItems);
        }
    }
}